package org.raftkv.raft.pb.impl;


import org.raftkv.commons.Tuple;
import org.raftkv.raft.exceptions.RaftException;
import org.raftkv.raft.pb.ConfChangeSingle;
import org.raftkv.raft.pb.IConfChange;
import org.raftkv.raft.pb.enums.ConfChangeTransition;
import org.raftkv.raft.pb.exceptions.IntOverflowRaftException;
import org.raftkv.raft.pb.exceptions.InvalidLengthRaftException;
import org.raftkv.raft.pb.exceptions.UnexpectedEOFException;
import org.raftkv.raft.pb.utils.PbUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

public class ConfChangeV2 implements IConfChange {

    private ConfChangeTransition transition;

    private List<ConfChangeSingle> changes;

    private byte[] context;

    @Override
    public ConfChangeV2 asV2() {
        return this;
    }

    @Override
    public Tuple<ConfChange, Boolean> asV1() {
        return new Tuple<>(new ConfChange(), false);
    }

    /**
     * 该方法返回两个布尔值，当且仅当此配置更改将使用联合共识时，第二个布尔值为真，如果它包含多个更改或明确请求使用联合共识，则会出现这种情况。
     * <br/>
     * 当第二个布尔值为真时，第一个布尔值只能为真，并指示联合状态是否将自动离开。
     */
    public Tuple<Boolean, Boolean> enterJoint() {
        if (this.transition != ConfChangeTransition.ConfChangeTransitionAuto || this.changes.size() > 1) {
            boolean autoLeave = false;
            switch (this.transition) {
                case ConfChangeTransitionAuto, ConfChangeTransitionJointImplicit:
                    autoLeave = true;
                    break;
                case ConfChangeTransitionJointExplicit:
                    break;
                default:
                    throw new RaftException(String.format("unknown transition: %s", this));
            }
            return new Tuple<>(autoLeave, true);
        }
        return new Tuple<>(false, false);
    }

    /**
     * 如果配置更改留下联合配置，则返回 true。
     */
    public boolean leaveJoint() {
        this.context = null;
        return new ConfChangeV2().equals(this);
    }

    public ConfChangeTransition getTransition() {
        return transition;
    }

    public void setTransition(ConfChangeTransition transition) {
        this.transition = transition;
    }

    public List<ConfChangeSingle> getChanges() {
        return changes;
    }

    public void setChanges(List<ConfChangeSingle> changes) {
        this.changes = changes;
    }

    public byte[] getContext() {
        return context;
    }

    public void setContext(byte[] context) {
        this.context = context;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ConfChangeV2 that = (ConfChangeV2) o;
        return transition == that.transition
                && Objects.equals(changes, that.changes)
                && Arrays.equals(context, that.context);
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(transition, changes);
        result = 31 * result + Arrays.hashCode(context);
        return result;
    }

    public byte[] marshal() {
        int size = this.size();
        byte[] data = new byte[size];
        int n = this.marshalToSizedBuffer(data);

        return Arrays.copyOfRange(data, 0, n);
    }

    private int size() {
        int l = 0;
        int n = 0;

        n += 1 + PbUtils.sovRaft(this.transition.ordinal());
        if (this.changes.size() > 0) {
            for (ConfChangeSingle e : this.changes) {
                l = e.size();
                n += 1 + l + PbUtils.sovRaft(l);
            }
        }

        if (this.context != null) {
            l = this.context.length;
            n += 1 + l + PbUtils.sovRaft(l);
        }

        return n;
    }

    private int marshalToSizedBuffer(byte[] data) {
        int i = data.length;
        int l = 0;
        if (this.context != null) {
            i -= this.context.length;
            System.arraycopy(this.context, 0, data, i, data.length);
            i = PbUtils.encodeVarintRaft(data, i, this.context.length);
        }

        if (this.changes.size() > 0) {
            for (int j = this.changes.size() - 1; j >= 0; j--) {
                int size = this.changes.get(j).marshalToSizedBuffer(data);
                i -= size;
                i = PbUtils.encodeVarintRaft(data, i, size);
                i--;
                data[i] = 0x12;
            }
        }
        i = PbUtils.encodeVarintRaft(data, i, this.transition.ordinal());
        i--;
        data[i] = 0x8;
        return data.length - i;
    }

    public void unmarshal(byte[] data) {
        int l = data.length;
        int index = 0;

        while (index < l) {
            int preIndex = index;
            long wire = 0;

            for (long shift = 0; ; shift += 7) {
                if (shift >= 64) {
                    throw new IntOverflowRaftException();
                }

                if (index >= l) {
                    throw new UnexpectedEOFException();
                }

                byte b = data[index];
                index++;
                wire |= (long) (b & 0x7F) << shift;
                if (b < 0x80) {
                    break;
                }
            }

            int filedNum = (int) (wire >> 3);
            int wireType = (int) (wire & 0x7);

            if (wireType == 4) {
                String errMsg = "ConfChangeV2: wiretype end group for non-group";
                LOGGER.error(errMsg);
                throw new RaftException(errMsg);
            }

            if (filedNum <= 0) {
                String errMsg = String.format("ConfChangeV2: illegal tag %d (wire type %d)", filedNum, wire);
                LOGGER.error(errMsg);
                throw new RaftException(errMsg);
            }

            switch (filedNum) {
                case 1:
                    if (wireType != 0) {
                        String errMsg = String.format("wrong wireType = %d for field transition", wireType);
                        LOGGER.error(errMsg);
                        throw new RaftException(errMsg);
                    }
                    this.transition = ConfChangeTransition.ConfChangeTransitionAuto;
                    int tempOrdinal = 0;
                    for (long shift = 0; ; shift += 7) {
                        if (shift >= 64) {
                            throw new IntOverflowRaftException();
                        }

                        if (index >= l) {
                            throw new UnexpectedEOFException();
                        }

                        byte b = data[index];
                        index++;
                        tempOrdinal |= (b & 0x7F) << shift;
                        try {
                            this.transition = ConfChangeTransition.values()[tempOrdinal];
                        } catch (Throwable ignore) {
                            ;
                        }
                        if (b < 0x80) {
                            break;
                        }
                    }
                    break;
                case 2:
                    if (wireType != 2) {
                        String errMsg = String.format("wrong wireType = %d for field changes", wireType);
                        LOGGER.error(errMsg);
                        throw new RaftException(errMsg);
                    }
                    int msgLen = 0;
                    for (long shift = 0; ; shift += 7) {
                        if (shift >= 64) {
                            throw new IntOverflowRaftException();
                        }

                        if (index >= l) {
                            throw new UnexpectedEOFException();
                        }

                        byte b = data[index];
                        index++;
                        msgLen |= (b & 0x7f) << shift;
                        if (b < 0x80) {
                            break;
                        }
                    }
                    if (msgLen < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    int postIndex = index + msgLen;
                    if (postIndex < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    if (postIndex > l) {
                        throw new UnexpectedEOFException();
                    }
                    try {
                        this.changes = new ArrayList<>();
                        this.changes.add(new ConfChangeSingle());
                        this.changes.get(this.changes.size() - 1).unmarshal(Arrays.copyOfRange(data, index, postIndex));
                    } catch (Exception e) {
                        this.context = new byte[0];
                    }
                    index = postIndex;
                    break;
                case 3:
                    if (wireType != 2) {
                        String errMsg = String.format("wrong wireType = %d for field context", wireType);
                        LOGGER.error(errMsg);
                        throw new RaftException(errMsg);
                    }
                    int byteLen = 0;
                    for (long shift = 0; ; shift += 7) {
                        if (shift >= 64) {
                            throw new IntOverflowRaftException();
                        }

                        if (index >= l) {
                            throw new UnexpectedEOFException();
                        }

                        byte b = data[index];
                        index++;
                        byteLen |= (b & 0x7F) << shift;
                        if (b < 0x80) {
                            break;
                        }
                    }
                    if (byteLen < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    int postIndex1 = index + byteLen;
                    if (postIndex1 < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    if (postIndex1 > l) {
                        throw new UnexpectedEOFException();
                    }
                    try {
                        this.context = Arrays.copyOfRange(data, index, postIndex1);
                    } catch (Exception e) {
                        this.context = new byte[0];
                    }
                    index = postIndex1;
                    break;
                default:
                    index = preIndex;
                    int skippy = PbUtils.skipRaft(Arrays.copyOf(data, index));
                    if (skippy < 0 || index + skippy < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    if (index + skippy > l) {
                        throw new UnexpectedEOFException();
                    }
                    index += skippy;
            }
        }

        if (index > l) {
            throw new UnexpectedEOFException();
        }
    }

}
